package org.codefilarete.stalactite.mapping;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import org.codefilarete.reflection.ReversibleAccessor;
import org.codefilarete.reflection.ValueAccessPoint;
import org.codefilarete.reflection.ValueAccessPointMap;
import org.codefilarete.stalactite.mapping.RowTransformer.TransformerListener;
import org.codefilarete.stalactite.sql.ddl.structure.Column;
import org.codefilarete.stalactite.sql.ddl.structure.Table;
import org.codefilarete.stalactite.sql.result.ColumnedRow;
import org.codefilarete.tool.collection.KeepOrderSet;
import org.codefilarete.tool.function.Converter;

/**
 * A very general contract for mapping a type to a database table. Not expected to be used as this (for instance it lacks deletion contract)
 * 
 * @author Guillaume Mary
 */
public interface Mapping<C, T extends Table<T>> {
	
	/**
	 * Returns columns that must be inserted
	 *
	 * @param c the instance to be inserted,
	 * 					may be null when this method is called to manage relation
	 * @return a mapping between columns that must be put in the SQL insert order and there values
	 */
	Map<Column<T, ?>, ?> getInsertValues(C c);
	
	/**
	 * Returns columns that must be updated because of change between 2 instances.
	 * 
	 * @param modified the modified instance,
	 * 					may be null when this method is called to manage relation
	 * @param unmodified the non modified instance or "previous state" (coming from database for instance),
	 * 					may be null to generate a rough update statement (allColumn doesn't matter in this case) 
	 * @param allColumns indicates if allColumns must be returned, even if they are not all modified (necessary for JDBC batch optimization)
	 * @return a mapping between columns that must be put in the SQL update order and there values,
	 * 			thus a distinction between columns to be updated and columns necessary to the where clause must be done, this is done through {@link UpwhereColumn},
	 * 			so returned value may contain duplicates regarding {@link Column} (they can be in update & where part, especially for optimist lock columns)	
	 */
	Map<UpwhereColumn<T>, ?> getUpdateValues(C modified, C unmodified, boolean allColumns);
	
	/**
	 * Transforms the given row into a new bean. It is not "alias proof" so it is not expected to be used in a select which columns haven't their
	 * name in the select clause, over all with alias.
	 * 
	 * @param row a row coming from a select clause of this entity
	 * @return a new bean which properties have been filled by row values
	 */
	C transform(ColumnedRow row);

	RowTransformer<C> getRowTransformer();
	
	/**
	 * Adds a column for insert. This column is not expected to be already mapped to a bean property of the &lt;T&gt; class
	 * Here are some use case examples :
	 * - getting the creation timestamp of a bean without the need to map it to a bean property,
	 * - completing a table with a value that is not expected by the bean but by the table 
	 * 
	 * It is not expected to use it to tamper with the value of a mapped property, even this is not forbidden and may work, it is not guaranteed to
	 * be a feature.
	 * 
	 * This method might have no purpose for many classes implementing {@link Mapping}.
	 * 
	 * @param valueProvider the column value provider
	 */
	default void addShadowColumnInsert(ShadowColumnValueProvider<C, T> valueProvider) {
		// does nothing by default
	}
	
	/**
	 * Adds a column for update. This column is not expected to be already mapped to a bean property of the &lt;T&gt; class
	 * Here are some use case examples :
	 * - timestamping a bean without the need to map the timestamp to a bean property,
	 * - completing a table with a value that is not expected by the bean but by the table 
	 * 
	 * It is not expected to use it to tamper with the value of a mapped property, even this is not forbidden and may work, it is not guaranteed to
	 * be a feature.
	 * 
	 * This method might have no purpose for many classes implementing {@link Mapping}.
	 *
	 * @param valueProvider the column value provider
	 */
	default void addShadowColumnUpdate(ShadowColumnValueProvider<C, T> valueProvider) {
		// does nothing by default
	}
	
	/**
	 * Adds a column to select. This column is not expected to be already mapped to a bean property of the &lt;T&gt; class.
	 *
	 * This method might have no purpose for many classes implementing {@link Mapping}.
	 *
	 * @param column the column to be read
	 * @param <O> Java type of the column value 
	 */
	default <O> void addShadowColumnSelect(Column<T, O> column) {
		// does nothing by default
	}
	
	void addPropertySetByConstructor(ValueAccessPoint<C> accessor);
	
	Map<ReversibleAccessor<C, ?>, Column<T, ?>> getPropertyToColumn();
	
	Map<ReversibleAccessor<C, ?>, Column<T, ?>> getReadonlyPropertyToColumn();
	
	default ValueAccessPointMap<C, Converter<Object, Object>> getReadConverters() {
		return new ValueAccessPointMap<>();
	}
	
	default ValueAccessPointMap<C, Converter<Object, Object>> getWriteConverters() {
		return new ValueAccessPointMap<>();
	}
	
	default Set<Column<T, ?>> getWritableColumns() {
		return new KeepOrderSet<>(getPropertyToColumn().values());
	}
	
	default Set<Column<T, ?>> getReadonlyColumns() {
		return new KeepOrderSet<>(getReadonlyPropertyToColumn().values());
	}
	
	/**
	 * Adds a transformer listener, optional operation
	 * @param listener the listener to be notified of transformation
	 */
	default void addTransformerListener(TransformerListener<C> listener) {
		// does nothing by default
	}
	
	/**
	 * Wrapper for {@link Column} placed in an update statement so it can distinguish if it's for the Update or Where part 
	 */
	class UpwhereColumn<T extends Table<T>> {
		
		/**
		 * Collects all columns from a set of {@link UpwhereColumn}. Helper method.
		 * 
		 * @param set a non null set
		 * @return all columns of the set
		 */
		public static <T extends Table<T>> Set<Column<T, ?>> getUpdateColumns(Set<? extends UpwhereColumn<T>> set) {
			Set<Column<T, ?>> updateColumns = new HashSet<>();
			for (UpwhereColumn<T> upwhereColumn : set) {
				if (upwhereColumn.update) {
					updateColumns.add(upwhereColumn.column);
				}
			}
			return updateColumns;
		}
		
		/**
		 * Collects all columns to be updated (not for the where clause) from a map of {@link UpwhereColumn}. Helper method.
		 * 
		 * @param map a non null map
		 * @return all columns to be updated
		 */
		public static <T extends Table<T>> Map<Column<T, Object>, ?> getUpdateColumns(Map<? extends UpwhereColumn<T>, ?> map) {
			Map<Column<T, Object>, Object> updateColumns = new HashMap<>();
			for (Entry<? extends UpwhereColumn<T>, ?> entry : map.entrySet()) {
				UpwhereColumn<T> upwhereColumn = entry.getKey();
				if (upwhereColumn.update) {
					updateColumns.put(upwhereColumn.column, entry.getValue());
				}
			}
			return updateColumns;
		}
		
		/**
		 * Collects all columns for the where clause (not for the update clause) from a map of {@link UpwhereColumn}. Helper method.
		 *
		 * @param map a non null map
		 * @return all columns for the where clause
		 */
		public static <T extends Table<T>> Map<Column<T, Object>, Object> getWhereColumns(Map<? extends UpwhereColumn<T>, ?> map) {
			Map<Column<T, Object>, Object> updateColumns = new HashMap<>();
			for (Entry<? extends UpwhereColumn<T>, ?> entry : map.entrySet()) {
				UpwhereColumn<T> upwhereColumn = entry.getKey();
				if (!upwhereColumn.update) {
					updateColumns.put(upwhereColumn.column, entry.getValue());
				}
			}
			return updateColumns;
		}
		
		private final Column<T, Object> column;
		private final boolean update;
		
		public UpwhereColumn(Column<T, ?> column, boolean update) {
			this.column = (Column<T, Object>) column;
			this.update = update;
		}
		
		public Column getColumn() {
			return column;
		}
		
		public boolean isUpdate() {
			return update;
		}
		
		/**
		 * Implemented to distinguish columns of the update clause from those of the where part, because the main purpose of this class is to be put
		 * in a {@link Map}.
		 * 
		 * @param obj another object
		 * @return true if the other object has the same column and has the same target : update or where part
		 */
		@Override
		public boolean equals(Object obj) {
			if (obj instanceof UpwhereColumn) {
				UpwhereColumn other = (UpwhereColumn) obj;
				return this.column.equals(other.column) && this.update == other.update;
			} else {
				return false;
			}
		}
		
		/**
		 * Based on hash code of the column
		 * @return column hash code
		 */
		@Override
		public int hashCode() {
			return this.column.hashCode();
		}
		
		/**
		 * Overridden to print "U" or "W" according to the purpose of this instance. Simplifies trace and eventual debug.
		 * @return the column's absolute name, suffixed by "U" or "W" according to the purpose of this instance 
		 */
		@Override
		public String toString() {
			return column.getAbsoluteName() + (update ? " (U)" : " (W)");
		}
	}
	
	/**
	 * Contract to provide values of some "unofficial" {@link Column}s out of a mapping strategy at insert and update
	 * time : those columns are not expected to be one of those mapped by properties but can be discriminator, list
	 * index, etc...
	 * <br/>
	 * An instance may reject to provide a value in some circumstances by overriding {@link #accept(Object)} (which
	 * returns true by default).
	 *
	 * Reader may be interested in getting {@link Column} value in select phase, then he may use
	 * {@link Mapping#addShadowColumnSelect(Column)}
	 *
	 * @param <C> bean type to read value from
	 * @param <T> table type
	 * @see #giveValue(Object)
	 * @see #accept(Object)
	 */
	interface ShadowColumnValueProvider<C, T extends Table<T>> {
		
		/**
		 * Made to determine if current column provider must be included to insert / update statement
		 * @param entity entity being persisted
		 * @return true if {@link #giveValue(Object)} must be call
		 */
		default boolean accept(C entity) {
			return true;
		}
		
		/**
		 * Gives the columns to be appended to the insert or update SQL statement.
		 * 
		 * @return columns to be appended to the insert or update SQL statement.
		 */
		Set<Column<T, ?>> getColumns();
		
		/**
		 * Expected to give values to be set in insert or update SQL Statement for given entity
		 * @param bean the entity that is being persisted
		 * @return values per {@link Column} (must be same column instances that were given to {@link #getColumns()})
		 */
		Map<Column<T, ?>, ?> giveValue(C bean);
	}
}
